Babel

将最新的 JS 语法(ES6/ES7),转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境。Babel 默认只转换新的 JS 句法 (syntax),而不转换新的 API。

Babel 内部所使用的语法解析器是 Babylon

主要功能

  • 语法转换
  • 通过 Polyfill 方式在目标环境中添加缺失的特性 (通过 @babel/polyfill 模块)
  • 源码转换 (codemods)

主要模块

  • @babel/parser:负责将代码解析为抽象语法树
  • @babel/traverse:遍历抽象语法树的工具,我们可以在语法树中解析特定的节点,然后做一些操作
  • @babel/core:代码转换,如 ES6 的代码转为 ES5 的模式

babel 工作流程

babel 的转译过程分为三个阶段:parsing、transforming、generating,以 ES6 代码转译为 ES5 代码为例,babel 转译的具体过程如下:

  1. 解析:将代码字符串解析成抽象语法树。babylon 将 ES6/ES7 代码解析成 AST。
    1. 词法分析:将代码(字符串)分割为 token 流,即语法单元成的数组。也叫扫描,是将字符流转换为记号流(tokens),它会读取我们的代码然后按照一定的规则合成一个个的标识。
    2. 语法分析:分析 token 流(上面生成的数组)并生成 AST。也称解析器,将词法分析出来的数组转换成树的形式,同时验证语法。语法如果有错的话,抛出语法错误。语法分析最终的结果成 AST。
  2. 转换:对抽象语法树进行转换操作。plugin 用 babel-traverse 对 AST 树进行遍历转译, 得到新的 AST 树。
  3. 再建:根据变换后的抽象语法树再生成代码字符串。新 AST 通过 babel-generator 转换成 ES5。

最重要的是第二步:转换,需要通过插件来定义转换规则, 也就是需要很多的插件来做转化。

presets 和 plugins 的区别

presets 其实是多个 plugin 的集合。如果某一个插件集合还不能满足我们的需要,就需要添加其他的插件。

例如需要 polyfill 的支持,但是又不想直接引入 polyfill(有弊端),可以使用 transform-runtime

transform-runtime 以及 stage-2 说一下他们的作用

env: env 则代指最新的标准,包括了 latest 和 es20xx 各年份

stage 几个阶段的区别

  1. Stage 0 - 稻草人: 只是一个想法,可能是 babel 插件。
  2. Stage 1 - 提案: 初步尝试。
  3. Stage 2 - 初稿: 完成初步规范。
  4. Stage 3 - 候选: 完成规范和浏览器初步实现。
  5. Stage 4 - 完成: 将被添加到下一年度发布。

babel 几个不同插件的有什么不同的作用,register-babel jsx-babel 等

babel-polyfill 和 babel-transform-runtime 的区别

由于 babel 默认只转换新的 JavaScript 语法,但对于一些新的 API 是不进行转化的(比如内建的 Promise、WeakMap,静态方法如 Array.from 或者 Object.assign),那么为了能够转化这些东西,我们就需要使用 babel-polyfill 这个插件。由于 babel-polyfill 是个运行时垫片,所以需要声明在 dependencies 而非 devDependencies 里。

由于使用 babel-polyfill,会产生以下问题:

  1. babel-polyfill 会将需要转化的 API 进行直接转化,这就导致用到这些 API 的地方会存在大量的重复代码
  2. babel-polyfill 是直接在全局作用域里进行垫片,所以会污染全局作用域

所以,babel 同时提供了 babel-plugin-transform-runtime 这一插件,它的好处在于:

  1. 需要用到的垫片,会使用引用的方式引入,而不是直接替换,避免了垫片代码的重复。
  2. 由于使用引用的方式引入,所以不会直接污染全局作用域。这就对于库和工具的开发带来了好处 但是 babel-plugin-transform-runtime 仍然不能单独作用。因为有一些静态方法,如"foobar".includes("foo")仍然需要引入 babel-polyfill 才能使用

babel、babel-polyfill 的区别:

babel-polyfill:模拟一个 es6 环境,提供内置对象如 Promise 和 WeakMap 引入 babel-polyfill 全量包后文件会变得非常大。它提供了诸如 Promise,Set 以及 Map 之类的内置插件,这些将污染全局作用域,可以编译原型链上的方法。

babel-plugin-transform-runtime & babel-runtime:转译器将这些内置插件起了别名 core-js,这样你就可以无缝的使用它们,并且无需使用 polyfill。但是无法编译原型链上的方法

runtime 编译器插件做了以下三件事:

  1. 当你使用 generators/async 函数时,自动引入 babel-runtime/regenerator 。
  2. 自动引入 babel-runtime/core-js 并映射 ES6 静态方法和内置插件。
  3. 移除内联的 Babel helper 并使用模块 babel-runtime/helpers 代替。

配置文件 .babelrc

存放在项目的根目录下面。基本格式如下:

{
  "presets": [],
  "plugins": []
}

presets (预设)字段设定转码规则,官方提供以下的规则集,你可以根据需要安装。

  • babel-preset-es2015 用于解析 ES6
  • babel-preset-react 用于解析 JSX
  • babel-preset-stage-0 用于解析 ES7

然后,将这些规则加入 .babelrc

  {
    "presets": [
      "es2015",
      "react",
      "stage-0"
    ],
    "plugins": []
  }

preset 预设

babel-polyfill

Babel 默认只转换新的 JavaScript 句法(syntax),而不转换新的 API,比如 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign)都不会转码。

举例来说,ES6 在 Array 对象上新增了 Array.from 方法。Babel 就不会转码这个方法。如果想让这个方法运行,必须使用 babel-polyfill,为当前环境提供一个垫片

babel-register

babel-register 字面意思能看出来,这是 babel 的一个注册器,它在底层改写了 node 的 require 方法,引入 babel-register 之后所有 require 并以.es6, .es, .jsx 和 .js 为后缀的模块都会经过 babel 的转译

Babel 是如何编译 let 和 const?

编译结果:

let value = "a";
// babel编译后:
var value = "a";

可以看到 Babel 是将 let 编译成了 var,那再来一个例子:

if (false) {
  let value = "a";
}
console.log(value); // value is not defined

如果 babel 将 let 编译为 var 因该打印 undefined,为何会报错呢,babel 是这样编译的:

if (false) {
  var _value = "a";
}
console.log(value);

babel 是改变量名,使内外层的变量名称不一样。 const 修改值时报错,以及重复声明报错怎么实现的呢?其实在编译时就报错了

编译 ES6 流程

1. 了解babel

说起编译es6,就必须提一下babel和相关的技术生态:

  1. babel-loader: 负责 es6 语法转化
  2. babel-preset-env: 包含 es6、7 等版本的语法转化规则
  3. babel-polyfill: es6 内置方法和函数转化垫片
  4. babel-plugin-transform-runtime: 避免 polyfill 污染全局变量

需要注意的是, babel-loaderbabel-polyfill。前者负责语法转化,比如:箭头函数;后者负责内置方法和函数,比如:new Set()

2. 安装相关库

这次,我们的package.json文件配置如下:

{
  "devDependencies": {
    "babel-core": "^6.26.3",
    "babel-loader": "^7.1.5",
    "babel-plugin-transform-runtime": "^6.23.0",
    "babel-preset-env": "^1.7.0",
    "webpack": "^4.15.1"
  },
  "dependencies": {
    "babel-polyfill": "^6.26.0",
    "babel-runtime": "^6.26.0"
  }
}

3. webpack中使用babel

babel的相关配置,推荐单独写在.babelrc文件中。下面,我给出这次的相关配置:

{
  "presets": [
    [
      "env",
      {
        "targets": {
          "browsers": ["last 2 versions"]
        }
      }
    ]
  ],
  "plugins": ["transform-runtime"]
}

webpack配置文件中,关于babel的调用需要写在module模块中。对于相关的匹配规则,除了匹配js结尾的文件,还应该去除node_module/文件夹下的第三库的文件(发布前已经被处理好了)。

module.exports = {
  entry: {
    app: "./app.js",
  },
  output: {
    filename: "bundle.js",
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /(node_modules)/,
        use: {
          loader: "babel-loader",
        },
      },
    ],
  },
};

4. 最后:babel-polyfill

我们发现整个过程中并没有使用babel-polyfill它需要在我们项目的入口文件中被引入,或者在webpack.config.js中配置。这里我们采用第一种方法编写app.js:

import "babel-polyfill";
let func = () => {};
const NUM = 45;
let arr = [1, 2, 4];
let arrB = arr.map((item) => item * 2);

console.log(arrB.includes(8));
console.log("new Set(arrB) is ", new Set(arrB));

命令行中进行打包,然后编写html文件引用打包后的文件即可在不支持es6规范的老浏览器中看到效果了。

Babel 的处理流程

Babel 整体的编译流程分为三个阶段:

  • parse: 通过 parser 把源代码转换成 AST
  • transform:通过遍历 AST,调用各种插件对 AST 进行转换,包括一些语法转换,代码优化等,最终生成新的 AST
  • generate: 把转换后的 AST 打印成目标代码,并生成 sourcemap

parse 阶段

parse 阶段的目的是把源码字符串转换成机器能够理解的 AST,这个过程分为词法分析、语法分析。

let name = "ljc";

我们定义了一个 name 变量

解析器第一步要做的就是把这个语句拆分成最小的不可拆分的单元

image-20210822114441105

生成 token 流,即语法单元组成的数组

[
  {
    "type": "Keyword",
    "value": "let"
  },
  {
    "type": "Identifier",
    "value": "name"
  },
  {
    "type": "Punctuator",
    "value": "="
  },
  {
    "type": "String",
    "value": "ljc"
  },
  {
    "type": "Punctuator",
    "value": ";"
  }
]

第二步就是语法分析

将上一步的 token 数据进行递归的组装,生成 AST,按照不同的语法结构,来把一组单词组合成对象,这个过程就是语法分析,比如上面的代码,就会生成这样的 AST

{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "name"
          },
          "init": {
            "type": "Literal",
            "value": "ljc",
            "raw": "\"ljc\""
          }
        }
      ],
      "kind": "let"
    }
  ],
  "sourceType": "module"
}

transform 阶段

transform 阶段的目的是对 AST 进行转换,这个过程分为遍历 AST 和调用插件。遍历的过程中处理到不同的 AST 节点会调用注册的相应的插件进行处理。也就是我们编写插件时注入的 visitor 函数。

如下就是 babel 插件大概的样子

module.exports = (babel) => {
  return {
    pre(path) {
      this.runtimeData = {};
    },
    visitor: {},
    post(path) {
      delete this.runtimeData;
    },
  };
};
  • visitor:指定 traverse 时调用的函数。
  • prepost 分别在遍历前后调用,可以做一些初始化和清理工作。比如初始化 runtimeData,遍历结束后删除 runtimeData

visitor 函数里,我们可以对 AST 进行增删改查,最终生成新的 AST。这样遍历完整个 AST 后,会得到一个新的 AST 和一些 sourcemap 信息。

这个阶段的核心是插件,插件使用 visitor 访问者模式定义了遇到特定的节点后如何进行操作。babel 将对 AST 树的遍历和对节点的增删改等方法放在了 @babel/traverse 包中。

图片来源于: Babel 插件通关秘籍open in new window

然后再通过 generate 阶段,将新的 AST 转换成 JS 代码。

generate 阶段

AST 转换完毕后,需要将 AST 重新生成 code。

generate 阶段会把 AST 转换成 code,这个过程是递归的,从根节点开始,遍历整个 AST,然后根据节点类型,生成对应的代码,不同的 AST 节点类型,会生成不同的代码。比如 VariableDeclaration 节点会生成 let name = 'ljc' (根据本文代码)这样的代码。

这样从 AST 根节点开始,递归的进行字符串拼接,最终生成的就是我们的代码。

以上就是 Babel 在编译时的流程,这里涉及到了几个关键的包。

  • @babel/parser:提供默认的 parse 方法用于解析
  • @babel/traverse: 封装了对 AST 树的遍历和节点的增删改查操作
  • @babel/generator: 提供给默认的 generate 方法用于代码生成。

总的来说,Babel 只负责串起整个流程,具体的编译交给 Babel 插件完成,核心的编译和生成 generator 的流程也能通过插件的方式进行扩展。

基于这样的设计, Babel 能够非常快速的跟进各种语言的变化。

了解了 Babel 的工作流程,我们继续看会 React 的 JSX 是如何被 Babel 转换的。

babel 把 ES6 转成 ES5 的原理是什么,有没有去研究

@babel/core 与 @babel/preset-env

写过 babel 插件吗?是用来干什么?怎么写的?

JSX 是什么? 怎么与 babel 有关系?

AST

抽象语法树(Abstract Syntax Tree)简称 AST,是源代码的抽象语法结构的树状表现形式。webpack、eslint 等很多工具库的核心都是通过抽象语法树这个概念来实现对代码的检查、分析等操作。

AST 能做什么

  1. 语法检查、代码风格检查、格式化代码、语法高亮、错误提示、自动补全等
  2. 代码混淆压缩
  3. 优化变更代码,改变代码结构等
  4. 给 await 函数自动注入 try catch (没有实践过)
  5. 移除 console.log 之类的。 webpack 插件做的事。

JSX

为什么能在 JS 中写 HTML 语法呢? 其实 JSX 是一种语法糖,可以在 JS 中编写 HTML,但实际上最终交由浏览器处理的还是 JS。

在这之间 JSX 是如何转换成 JS 代码的呢?借助一些构建工具,例如 babel,本文就通过 Babel 来介绍,JSX 是如何转译成 JS 代码

关于 createElement

JSX 会通过 Babel 最终转化成 React.createElement 的这种形式

function test() {
  return <div class="hello">world</div>;
}
// 转换成
("use strict");
function test() {
  return /*#__PURE__*/ React.createElement(
    "div",
    {
      class: "hello",
    },
    "world"
  );
}
  • 第一个参数是要创建的元素的 Tag 值
  • 第二个参数是我们传给元素的 props 值,在生成的 JS 代码中,是以一个普通对象,以键值对的方式存在。
  • 第三个参数是 children

React JSX to JS

JSX 转换成 JS 借助 Babel 的 transform 插件 babel-plugin-transform-react-jsx,这个插件的作用是将 JSX 转换成 React.createElement 方法调用。

在前面的流程中我们知道,Babel 的编译流程是通过插件来实现的,那么 babel-plugin-transform-react-jsx 插件是如何工作的呢?

transform 阶段,可以对 AST 进行增删改查,生成新的 AST。

我们看到 babel-plugin-transform-react-jsx 的源代码中

visitor 函数中,会对不同的 JSX 类型节点有不同的处理,比如 JSXElement 节点,会调用 jsxElement 方法,这个方法会返回一个 JSXElement 的 AST 节点。

在这个阶段处理的是 AST 对象,我们再来看看一段 JSX 它的 AST 是怎样的结构?

function test() {
  return (
    <div class="hello">
      <span>word</span>
    </div>
  );
}

在这段代码中,JSX 节点的 AST 结构如下:

我们可以在图中看到 return 语句的 AST 节点,它的 type 是 ReturnStatement,它的 argument 是一个 JSXElement 节点,这个节点的 type 是 JSXElement,它的 openingElement 是一个 JSXOpeningElement 节点,它的 type 是 JSXOpeningElement,它的 name 是一个 JSXIdentifier 节点,它的 type 是 JSXIdentifier,它的 name 是 div

Babel 在从根节点开始遍历 AST 树的时候,就会遍历到这个 JSXElement 节点,然后调用 babel-plugin-transform-react-jsx 插件中 visitor 中的 jsxElement 方法

JSXElement: {
    exit(path, file) {
    let callExpr;
    if (
        get(file, "runtime") === "classic" ||
        shouldUseCreateElement(path)
    ) {
        callExpr = buildCreateElementCall(path, file);
    } else {
        callExpr = buildJSXElementCall(path, file);
    }
    // 用处理完的 AST 节点替换原来的 JSXElement 节点
    path.replaceWith(t.inherits(callExpr, path.node));
    },
},

可以看到这里会通过 shouldUseCreateElement 来判断是否转成 React.createElement 方法调用,如果是的话,就会调用 buildCreateElementCall 方法,如果不是的话,就会调用 buildJSXElementCall 方法。

那么这里为什么要判断是否转成 React.createElement 方法调用呢?我们来看看 shouldUseCreateElement 方法

// We want to use React.createElement, even in the case of
// jsx, for <div {...props} key={key} /> to distinguish it
// from <div key={key} {...props} />. This is an intermediary
// step while we deprecate key spread from props. Afterwards,
// we will stop using createElement in the transform.
function shouldUseCreateElement(path: NodePath<JSXElement>) {
  const openingPath = path.get("openingElement");
  const attributes = openingPath.node.attributes;

  let seenPropsSpread = false;
  for (let i = 0; i < attributes.length; i++) {
    const attr = attributes[i];
    if (seenPropsSpread && t.isJSXAttribute(attr) && attr.name.name === "key") {
      return true;
    } else if (t.isJSXSpreadAttribute(attr)) {
      seenPropsSpread = true;
    }
  }
  return false;
}

这个方法很特别,我们通过在源码中的注释可以得知,这个判断是为了区分 <div {...props} key={key} /><div key={key} {...props} /> 这两种情况的,为什么需要这个判断呢,我在 React 的 Github issueopen in new window 中找到了答案

我们看看下面这段代码

let obj = { key: "bar" }

// 1. Key Before Spread
<div key="foo" {...obj} />.key // "bar"

// 2. Key After Spread
<div {...obj} key="foo" />.key // "foo"

在这段代码中,key 存在被 props 覆盖的情况,但是在 React 的定义中,keyprops 的一部分,所以在 React 中,key 的优先级是最高的,所以在 React 中,key 的值是 foo,而不是 bar。这里应该不能被覆盖,因此这里采用了一个条件分支来处理 key 先后的情况下,进行不同的转换。

在官方的解释中也印证了这点,并对这个问题进行了未来的规划,从目前的源码来看,这里还处于 Today 的阶段,但这个 comment 的时间是 2020 年了...

在 Babel 源码的 buildJSXElementCall 方法中,并没有看到有 warning 的逻辑,所以这里应该不会 warn,因此觉得当前还是 Today 阶段

抛开这个不纠结,继续看到生成 createElement 的逻辑

生成 createElement

buildCreateElementCall 方法中

  • 首先会通过 getTag 方法来获取 tag,这个方法很简单,就是获取 openingElementname
  • 通过 buildCreateElementOpeningElementAttributes 方法来获取 attributes 的值,具体的不展开了
function buildCreateElementCall(path: NodePath<JSXElement>, file: PluginPass) {
  const openingPath = path.get("openingElement");

  return call(file, "createElement", [
    getTag(openingPath),
    buildCreateElementOpeningElementAttributes(
      file,
      path,
      openingPath.get("attributes")
    ),
    // @ts-expect-error JSXSpreadChild has been transformed in convertAttributeValue
    ...t.react.buildChildren(path.node),
  ]);
}

这里的关键应该看到这个 call 方法,这个方法是用来生成 React.createElement 方法调用的,通过 t.callExpression 来生成,这里的 get 方法是用来获取 id 的,这里的 id 是在 visitor 中定义的,传入的是 createElement,所以这里就是生成了 React.createElement 方法调用

// get 方法
const get = (pass: PluginPass, name: string) =>
  pass.get(`@babel/plugin-react-jsx/${name}`);

function call(
  pass: PluginPass,
  name: string,
  args: CallExpression["arguments"]
) {
  const node = t.callExpression(get(pass, `id/${name}`)(), args);
  if (PURE_ANNOTATION ?? get(pass, "defaultPure")) annotateAsPure(node);
  return node;
}

get 和 set 函数

在上面我们看到了 get 方法,对应的还有 set 方法,这里我们再讲讲这部分的东西

const get = (pass: PluginPass, name: string) =>
  pass.get(`@babel/plugin-react-jsx/${name}`);
const set = (pass: PluginPass, name: string, v: any) =>
  pass.set(`@babel/plugin-react-jsx/${name}`, v);

visitorprogram 函数中会通过当前的运行上下文环境来决定是否需要生成 jsx 的 id

  • 如果是经典(classic)的方式,也就是手动引入 React 的方式,那么就只需要生成 createElementfragment 的 id
  • 如果是自动引入 React 时,就还需要设置 jsx 的 id
if (runtime === "classic") {
    ...
    const createElement = toMemberExpression(pragma)
    const fragment = toMemberExpression(pragmaFrag)

    set(state, "id/createElement", () => t.cloneNode(createElement))
    set(state, "id/fragment", () => t.cloneNode(fragment))

    set(state, "defaultPure", pragma === DEFAULT.pragma)
} else if (runtime === "automatic") {
    ...
    const define = (name: string, id: string) =>
        set(state, name, createImportLazily(state, path, id, source))

    define("id/jsx", development ? "jsxDEV" : "jsx")
    define("id/jsxs", development ? "jsxDEV" : "jsxs")
    define("id/createElement", "createElement")
    define("id/fragment", "Fragment")

    set(state, "defaultPure", source === DEFAULT.importSource)
}

在上面我们可以看到在 set 函数执行时,会传入一个回调,注册对应的方法,当我们调用 get 方法时,就会执行这个回调,然后返回对应的值

const DEFAULT = {
  pragma: "React.createElement",
  pragmaFrag: "React.Fragment",
};

const createElement = toMemberExpression(pragma);
const fragment = toMemberExpression(pragmaFrag);

可以看到这里的 toMemberExpression 方法是用来将 React.createElement 转换成 React.createElement 的 AST 节点的

toMemberExpression 方法中会遍历传入的 id,也就是这个 DEFAULT 对象定义的值,然后通过 t.identifier 方法将每个 id 转换成对应的 AST 节点,然后通过 t.memberExpression 方法将每个节点转换成对应的 AST 节点,最后通过 reduce 方法将每个节点转换成一个 MemberExpression 的 AST 节点

function toMemberExpression(id: string): Identifier | MemberExpression {
  return (
    id
      .split(".")
      .map((name) => t.identifier(name))
      // @ts-expect-error - The Array#reduce does not have a signature
      // where the type of initialial value differs from callback return type
      .reduce((object, property) => t.memberExpression(object, property))
  );
}

这也解释了为什么调用 call 函数能够生成 JSX 对应 createElement 方法对应的 AST 了

替换 JSX 结构

JSXElement 方法的结尾,我们可以看到,这里是通过 buildCreateElementCall 方法来生成 React.createElement 方法调用的,然后通过 path.replaceWith 来替换掉原来的 JSX 结构,得到一个由 React.createElement 方法调用组成的 AST

JSXElement: {
    exit(path, file) {
      ...
      // 用处理完的 AST 节点替换原来的 JSXElement 节点
+     path.replaceWith(t.inherits(callExpr, path.node));
    },
}

经过这些就完成了 JSX 到 JS AST 的转换了,当然这里还有一些特殊的节点没有涉及到,比如 React 中的 Fragment 节点,也有自己的处理逻辑

大致的思路就是将 <></> 转换成 <React.Fragment></React.Fragment>,然后再通过 buildCreateElementCall 方法来生成 React.createElement 方法调用,最后通过 path.replaceWith 来替换掉原来的 JSX 结构

中间的过程和 JSXElement 是一样的,不同点就是多了 React.Fragment 的转换

JSXFragment(path, file) {
    // <>...</>  ->  <React.Fragment>...</React.Fragment>

    const frag = memberExpressionToJSX(get(file, "id/fragment")())

    path.replaceWith(
        t.inherits(
            t.jsxElement(
                t.inherits(
                    t.jsxOpeningElement(frag, []),
                    path.node.openingFragment,
                ),
                t.jsxClosingElement(t.cloneNode(frag)),
                path.node.children,
            ),
            path.node,
        ),
    )
}

最后再在 generate 阶段完成 AST 到 JS 的转换,这样整个 JSX 就转化完成了

以上就是 JSX 到 JS 的转换过程,更详细的我们可以直接看 plugin 的源码,这部分的代码还算简单

总结

React 的 JSX 会被 Babel 的 @babel/plugin-transform-react-jsx 插件转换成 React.createElement 方法调用,这个插件的核心就是通过 visitor 函数来遍历 AST,然后对不同类型的节点进行处理,比如 JSXElement,JSXFragment 等,最后将 JSX 转换成 React.createElement 方法调用,得到一个由 createElement fn 组成的 AST,最后再在 generate 阶段完成 AST 到 JS 的转换

下一篇,将通过手写 Babel 插件来实现 JSX To JS AST 的转换,这样我们就能更加深入的了解 Babel 的插件机制以及 React JSX transform 的实现

是否可以在没有 JSX 的情况下使用 React?

是的,使用 React 不强制使用 JSX。实际上,当你不想在构建环境中配置编译环境时,这是很方便的。每个 JSX 元素只是调用 React.createElement(component, props, ...children) 的语法糖。例如,让我们来看一下使用 JSX 的 greeting 示例:

class Greeting extends React.Component {
  render() {
    return <div>Hello {this.props.message}</div>;
  }
}

ReactDOM.render(<Greeting message="World" />, document.getElementById("root"));

你可以在没有 JSX 的情况下编写相同的功能,如下所示:

class Greeting extends React.Component {
  render() {
    return React.createElement("div", null, `Hello ${this.props.message}`);
  }
}

ReactDOM.render(
  React.createElement(Greeting, { message: "World" }, null),
  document.getElementById("root")
);

什么是 JSX?

一个类似 XML 的语法扩展。它是 React.createElement() 函数提供语法糖.

在下面的示例中,<h1> 内的文本标签会作为 JavaScript 函数返回给渲染函数。

class App extends React.Component {
  render() {
    return (
      <div>
        <h1>{"Welcome to React world!"}</h1>
      </div>
    );
  }
}

以上示例 render 方法中的 JSX 将会被转换为以下内容:

React.createElement(
  "div",
  null,
  React.createElement("h1", null, "Welcome to React world!")
);

编译函数,指定 h 函数。

/** @jsx h */

jsx 文件顶部可以指定 pragma, 默认情况下是 React.createElement 这也就是为什么,js 文件中需要默认引入 react 的原因。

JSX 如何防止注入攻击?

React DOM 会在渲染 JSX 中嵌入的任何值之前对其进行转义。因此,它确保你永远不能注入任何未在应用程序中显式写入的内容。

const name = response.potentiallyMaliciousInput;
const element = <h1>{name}</h1>;

这样可以防止应用程序中的 XSS(跨站点脚本)攻击。

react 的 class 组件为什么需要在开头 import react? 去掉能不能跑?

之前我记得是不能的。 因为 JSX 代码会被编译成 React.createElement

Last Updated:
Contributors: yiliang114